/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { NodePredicate, PropType } from '~/type';
import { getDslAdapter } from '~/dsl/Adapter';
import { DslRule, EngineCapability } from '~/dsl/Defines';
import { DefineRuleContext, RuleContext } from '~/dsl/Context';
import { EngineCapabilityHint } from '~/extensions/hint/CapabilityHint';

/**
 * Start declaration of a rule.
 *
 * The provided properties will persist throughout the rule's lifecycle.
 * You can access the properties using the `props` field in the context.
 *
 * It starts a rule declaration targeting any underlying engine. If you only intend
 * to write a rule for a specific engine, you can use {@link native} instead.
 *
 * @typeParam PropsType The type of the properties, can be inferred if all fields
 * are given. Explicitly specify it if there are any optional fields.
 * @typeParam HintType Type hint for specific engine.
 * @typeParam U The constructed context type.
 *
 * @param param The properties to be passed to the rule.
 * You can also pass a builder function that takes the context as an argument.
 *
 * @example
 *
 * If all fields are given, you can omit `PropsType`.
 *
 * ```typescript
 * rule({ name: 'example' })
 * ```
 *
 * If there are any optional fields, you need to specify `PropsType` for better type inference.
 *
 * ```typescript
 * rule<{ name: string; maxLength?: number }>({ name: 'example' })
 * ```
 *
 * You can also define the properties as an interface or type for better readability.
 *
 * ```typescript
 * interface ExampleProps {
 *     name: string;
 *     maxLength?: number;
 * }
 *
 * rule<ExampleProps>({ name: 'example' })
 * ```
 *
 * If this initialization process requires some context information, you can use a builder function.
 * For the context, you can use destructuring to directly access the properties.
 *
 * ```typescript
 * rule<ExampleProps>(({ config }) => {
 *     return { name: 'example', maxLength: config.maxLength };
 * })
 * ```
 *
 * @category DSL
 */
export function rule<
    P,
    H extends EngineCapabilityHint = EngineCapabilityHint,
    U extends DefineRuleContext<P, H> = DefineRuleContext<P, H>,
>(param?: P | ((context: RuleContext<H>) => P)): AwaitMatch<H, U> {
    if (param === undefined) {
        param = {} as P;
    }

    if (typeof param === 'function') {
        return ruleImplAction<P, H, U>(param as (context: RuleContext<H>) => P);
    } else {
        return ruleImplValue<P, H, U>(param);
    }
}

/**
 * Specify the native engine type hint for the rule.
 *
 * @typeParam HintType The type hint for the target engine.
 *
 * @returns A pre-configured {@link rule} targeting the specified engine.
 *
 * @example
 *
 * If the engine is supported, you can find the type hint in this package.
 * For example, if the engine is `Xxx`, the type hint is `XxxHint`.
 * Simply replace `rule` with `native` to use the type hint.
 *
 * ```typescript
 * native<XxxHint>()({ name: 'example' })
 * ```
 *
 * @category DSL
 */
export function native<H extends EngineCapabilityHint>(
    options?: PropType<H, 'options'>
): <P, U extends DefineRuleContext<P, H> = DefineRuleContext<P, H>>(
    param?: ((context: RuleContext<H>) => P) | P
) => AwaitMatch<H, U> {
    // Curried function to allow partial application of the type parameter.
    return function <P, U extends DefineRuleContext<P, H> = DefineRuleContext<P, H>>(
        param?: P | ((context: RuleContext<H>) => P)
    ): AwaitMatch<H, U> {
        if (options) {
            let action: (context: RuleContext<H>) => P;
            if (param === undefined) {
                action = (context: RuleContext<H>) => {
                    context.options = options;
                    return {} as P;
                };
            } else if (typeof param === 'function') {
                action = (context: RuleContext<H>) => {
                    context.options = options;
                    return (param as (context: RuleContext<H>) => P)(context);
                };
            } else {
                action = (context: RuleContext<H>) => {
                    context.options = options;
                    return param as P;
                };
            }
            return rule<P, H, U>(action);
        }

        return rule<P, H, U>(param);
    };
}

function ruleImplValue<P, H extends EngineCapabilityHint, U extends DefineRuleContext<P, H>>(
    props: P
): AwaitMatch<H, U> {
    return ruleImplAction<P, H, U>(() => props);
}

function ruleImplAction<P, H extends EngineCapabilityHint, U extends DefineRuleContext<P, H>>(
    init: (context: RuleContext<H>) => P
): AwaitMatch<H, U> {
    return getDslAdapter().entry(init);
}

/**
 * Ready to perform a preliminary check on the node to determine if it should
 * be further processed.
 *
 * The predicate/check used here should be a lightweight operation, as it will be
 * invoked for every node in the AST. At the same time, it should be as specific as
 * possible to avoid matching too many irrelevant nodes.
 *
 * @typeParam C Rule context type.
 *
 * @category DSL
 */
export interface AwaitMatch<H extends EngineCapabilityHint, C> {
    /**
     * Match nodes with AST information only.
     *
     * Most of the time, it is sufficient to filter the nodes with AST alone.
     * It can be any built-in {@link NodePredicate} or your custom one.
     * See {@link NodePredicate} for how to define custom predicates.
     *
     * @typeParam T The desired AST node type. This will be used in the following
     * `when` and `report` methods as the type of the node.
     *
     * @param predicate The predicate to match the node.
     */
    match<T extends PropType<H, 'node'>>(predicate: NodePredicate<T>): AwaitWhen<PropType<H, 'node'>, T, C>;

    /**
     * Match nodes with AST information and context.
     *
     * This is useful if you require additional information from the context.
     *
     * @typeParam T The desired AST node type. This will be used in the following
     * `when` and `report` methods as the type of the node.
     *
     * @param predicate The predicate to match the node.
     *
     * @example
     *
     * ```typescript
     * rule().match((node, { config }): node is ts.VoidExpression => {
     *     if (config.checkVoid) {
     *         return VoidExpression.is(node);
     *     } else {
     *         return false;
     *     }
     * })
     * ```
     */
    match<T extends PropType<H, 'node'>>(
        predicate: (node: PropType<H, 'node'>, context: C) => node is T
    ): AwaitWhen<PropType<H, 'node'>, T, C>;
}

export type WhenResult<N> = boolean | N | N[] | undefined;

/**
 * Ready to perform a detailed check on the matched node.
 *
 * `when` is not required, you can report issues directly after `match`.
 * In this case, the report action will be invoked for every matched node.
 * In other words, the default `when` action returns `true` for every node.
 *
 * @typeParam N The base node type.
 * @typeParam T The matched node type.
 * @typeParam C The context type.
 *
 * @category DSL
 */
export interface AwaitWhen<N, T, C> extends AwaitReport<N, C> {
    /**
     * Perform a detailed check on the matched node.
     *
     * For the `action`, return `true` to indicate that the node is matched, otherwise
     * return `false`. If `true` is returned, the `report` method will be invoked
     * immediately afterward.
     *
     * If you want to report issue on nodes other than the matched one, you can return
     * the desired node(s) instead of boolean value. In this case, the `report` method
     * will be invoked for each returned node. Return `undefined` to skip reporting.
     *
     * @param action Action to perform on the node.
     *
     * @example
     *
     * For simple checks, you can use `node` alone.
     *
     * ```typescript
     * .when((node) => { ... })
     * ```
     *
     * If you need to access the context, you can access it in a destructured way.
     *
     * ```typescript
     * // access custom property only
     * .when((node, { props }) => { ... })
     *
     * // access API and custom property
     * .when((node, { api, props }) => { ... })
     * ```
     */
    when(action: (node: T, context: C) => WhenResult<N>): AwaitReport<N, C>;
}

/**
 * Ready to report issues.
 *
 * @typeParam N The base node type.
 * @typeParam C The context type.
 * @typeParam H The engine capability hint type.
 *
 * @category DSL
 */
export interface AwaitReport<
    N,
    C,
    H extends EngineCapabilityHint = C extends DefineRuleContext<any, infer H> ? H : never,
> {
    /**
     * Report an issue using underlying engine's API.
     *
     * The `action` will be invoked immediately after the callback defined in the
     * `when` method if it returns `true`.
     *
     * To report an issue, refer to the underlying engine's documentation for the
     * detailed issue type and format. Then, report it using the `report` method
     * given in {@link EngineCapability}, which can be accessed in the context.
     *
     * The returned `DslRule` by default has `any` type as its type parameter.
     * When you implement this interface for the DSL adapter, you should specify
     * the concrete type of the rule, which is the engine's native rule type.
     *
     * @param action Action to perform on the node.
     *
     * @example
     *
     * If you omit `when`, it is equivalent to the following rule.
     *
     * ```typescript
     * rule()
     *     .match(BinaryExpression)
     *     .when(() => true)
     *     .report((node, { api }) => {
     *         api.report({ ... });
     *     })
     * ```
     */
    report(action: (node: N, context: C) => void): DslRule;

    /**
     * Report an issue using a template provided by the engine.
     *
     * For issues with general format, you may use the issue template provided by the engine.
     *
     * @param template The template to be used for the report.
     */
    report(template: PropType<H, 'issueTemplate'>): DslRule;
}
